/**
 * 
 */
package xj.toolkit.bean.tlv.decode.decoders;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import xj.toolkit.bean.bytebean.ByteFieldToDesc;
import xj.toolkit.bean.bytebean.codec.BeanCodecUtil;
import xj.toolkit.bean.bytebean.core.ByteFieldCodec;
import xj.toolkit.bean.bytebean.core.ByteFieldDesc;
import xj.toolkit.bean.bytebean.core.DecContextFactory;
import xj.toolkit.bean.bytebean.core.DecResult;
import xj.toolkit.bean.bytebean.core.FieldCodecCategory;
import xj.toolkit.bean.bytebean.core.FieldCodecProvider;
import xj.toolkit.bean.tlv.annotation.TlvType;
import xj.toolkit.bean.tlv.decode.TLVDecodeContext;
import xj.toolkit.bean.tlv.decode.TLVDecodeContextFactory;
import xj.toolkit.bean.tlv.decode.TLVDecoder;
import xj.toolkit.bean.tlv.decode.TLVDecoderOfBean;
import xj.toolkit.bean.tlv.meta.TLVFieldMetainfo;
import xj.toolkit.bean.util.meta.Int2TypeMetainfo;
import xj.toolkit.util.ByteUtils;

/**
 * @author hp
 *
 */
public class BeanMixedTLVDecoder implements TLVDecoderOfBean {

	private static final Logger LOG = LoggerFactory.getLogger(BeanMixedTLVDecoder.class);

	private TLVDecodeContextFactory tlvDecodeContextFactory;
	private DecContextFactory byteDecContextFactory;
	private FieldCodecProvider byteCodecProvider;

	private BeanCodecUtil util = null;
	
	public BeanMixedTLVDecoder(boolean fieldDescCached) {
		super();
		util = new BeanCodecUtil(new ByteFieldToDesc(), fieldDescCached);
	}

	/**
	 * @return the byteCodecProvider
	 */
	public FieldCodecProvider getByteCodecProvider() {
		return byteCodecProvider;
	}

	/**
	 * @param byteCodecProvider
	 *            the byteCodecProvider to set
	 */
	public void setByteCodecProvider(FieldCodecProvider byteCodecProvider) {
		this.byteCodecProvider = byteCodecProvider;
	}

	/**
	 * @return the tlvDecodeContextFactory
	 */
	public TLVDecodeContextFactory getTlvDecodeContextFactory() {
		return tlvDecodeContextFactory;
	}

	/**
	 * @param tlvDecodeContextFactory
	 *            the tlvDecodeContextFactory to set
	 */
	public void setTlvDecodeContextFactory(TLVDecodeContextFactory tlvDecodeContextFactory) {
		this.tlvDecodeContextFactory = tlvDecodeContextFactory;
	}

	/**
	 * @return the byteDecContextFactory
	 */
	public DecContextFactory getByteDecContextFactory() {
		return byteDecContextFactory;
	}

	/**
	 * @param byteDecContextFactory
	 *            the byteDecContextFactory to set
	 */
	public void setByteDecContextFactory(DecContextFactory byteDecContextFactory) {
		this.byteDecContextFactory = byteDecContextFactory;
	}

	/**
	 * @see com.skymobi.bean.tlv.decode.decoders.TLVDecoderOfBean#
	 * getDecodeContextFactory()
	 */
	public TLVDecodeContextFactory getDecodeContextFactory() {
		return tlvDecodeContextFactory;
	}

	public Object decode(int tlvLength, byte[] tlvValue, final TLVDecodeContext ctx) {

		if (tlvValue.length < tlvLength) {
			throw new RuntimeException("BeanMixedTLVDecoder, too few bytes.");
		}

		final Class<?> valueClazz = ctx.getValueType();
		if (null == valueClazz) {
			throw new RuntimeException("BeanMixedTLVDecoder, invalid value type.");
		}

		Object target = null;
		try {
			target = List.class.isAssignableFrom(valueClazz) ? new ArrayList<Object>() : valueClazz.newInstance();
		} catch (InstantiationException e) {
			LOG.error("BeanMixedTLVDecoder:", e);
		} catch (IllegalAccessException e) {
			LOG.error("BeanMixedTLVDecoder:", e);
		}

		if (null == target) {
			throw new RuntimeException("BeanMixedTLVDecoder, can not create instance.");
		}

		int orgByteLen = tlvValue.length;

		tlvValue = decodeByteFields(tlvValue, valueClazz, target);
		tlvLength -= (orgByteLen - tlvValue.length);

		decodeTLVFields(tlvLength, tlvValue, ctx, target);

		return target;
	}

	/**
	 * @param tlvLength
	 * @param tlvValue
	 * @param ctx
	 * @param target
	 */
	@SuppressWarnings("unchecked")
	private void decodeTLVFields(final int tlvLength, final byte[] tlvValue, final TLVDecodeContext ctx,
			final Object target) {
		int offset = 0;
		while (offset + 8 <= tlvLength) {

			final byte[] tagBytes = ArrayUtils.subarray(tlvValue, offset, offset + 4);
			final int tag = ctx.getNumberCodec().bytes2Int(tagBytes, 4);

			offset += 4;
			final byte[] lenBytes = ArrayUtils.subarray(tlvValue, offset, offset + 4);
			final int len = ctx.getNumberCodec().bytes2Int(lenBytes, 4);
			offset += 4;

			if (len < 0) {
				LOG.error("tag {}'s len less than 0, bytes detail {}", tag,
						ByteUtils.bytesAsHexString(ArrayUtils.subarray(tlvValue, offset - 8, tlvValue.length), 1024));
				throw new RuntimeException("tag [" + tag + "]'s len [" + len + "] less than 0");
			}

			final Int2TypeMetainfo typeMetainfo = ctx.getTypeMetainfo();
			final Class<?> type = typeMetainfo.find(tag);
			if (null == type) {
				// unknow tag, just ignore
				LOG.info("unknow tag:" + tag + ", just ignore.");
				offset += len;
				continue;
			}

			final TLVDecoder decoder = ctx.getDecoderRepository().getDecoderOf(type);
			if (null == decoder) {
				// unknow tag, just ignore
				LOG.info("unknow decoder for [tag]:" + tag + ",[type]:" + type + " just ignore.");
				offset += len;
				continue;
			}

			final byte[] valueBytes = ArrayUtils.subarray(tlvValue, offset, offset + len);
			offset += len;

			final TLVFieldMetainfo fieldMetainfo = ctx.getFieldMetainfo();
			final Field field = fieldMetainfo.get(tag);
			Object bean = null;
			try {
				bean = decoder.decode(len, valueBytes, tlvDecodeContextFactory.createDecodeContext(type, field));
			} catch (RuntimeException e) {
				LOG.error("Decode tag {}({}) error!", new String(Hex.encodeHex(tagBytes)).toUpperCase(), tag);
				throw e;
			}

			final TlvType param = field.getAnnotation(TlvType.class);
			Class<?> fieldType = param.type();
			if (TlvType.class.equals(fieldType)) {
				fieldType = field.getType();
			}
			if (List.class.isAssignableFrom(fieldType)) {
				try {
					List<Object> list = (List<Object>) field.get(target);
					if (null == list) {
						list = new ArrayList<Object>();
						field.set(target, list);
					}
					list.add(bean);
				} catch (IllegalArgumentException e) {
					LOG.error("BeanMixedTLVDecoder:", e);
				} catch (IllegalAccessException e) {
					LOG.error("BeanMixedTLVDecoder:", e);
				}
			} else {
				try {
					field.set(target, bean);
				} catch (IllegalArgumentException e) {
					LOG.error("BeanMixedTLVDecoder:", e);
				} catch (IllegalAccessException e) {
					LOG.error("BeanMixedTLVDecoder:", e);
				}
			}
		}
	}

	/**
	 * @param tlvValue
	 * @param valueClazz
	 * @param target
	 * @return
	 */
	private byte[] decodeByteFields(final byte[] tlvValue, final Class<?> valueClazz, final Object target) {
		final List<ByteFieldDesc> desces = util.getFieldDesces(valueClazz);
		
		if (desces.isEmpty()) {
			return tlvValue;
		}
		

		byte[] bytes = tlvValue;

		// include ByteField annotation
		final ByteFieldCodec anyCodec = byteCodecProvider.getCodecOf(FieldCodecCategory.ANY);

		for (ByteFieldDesc desc : desces) {

			final Field field = desc.getField();

			final Class<?> fieldClass = field.getType();

			final DecResult ret = anyCodec
					.decode(byteDecContextFactory.createDecContext(bytes, fieldClass, target, desc));
			final Object fieldValue = ret.getValue();

			bytes = ret.getRemainBytes();

			field.setAccessible(true);
			try {
				field.set(target, fieldValue);
			} catch (IllegalArgumentException e) {
				LOG.error("failed to set value [{}] to field[{}]", fieldValue, field);
			} catch (IllegalAccessException e) {
				LOG.error("failed to set value [{}] to field[{}]", fieldValue, field);
			}
		}

		return bytes;
	
	}

	public Class<?>[] getFieldType() {
		return new Class<?>[] { java.lang.Object.class };
	}
}
